package fr.kptn.utils.communication.xauth;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.HttpURLConnection;
import java.net.URLEncoder;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.NameValuePair;
import org.apache.http.ParseException;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.HttpDelete;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpPut;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.protocol.HTTP;
import org.apache.http.util.EntityUtils;

import fr.kptn.utils.communication.xauth.exceptions.XAuthCommunicatorException;
import fr.kptn.utils.communication.xauth.helpers.XAuthAccessInfos;
import fr.kptn.utils.communication.xauth.helpers.XAuthParams;
import fr.kptn.utils.communication.xauth.utils.Base64;
import fr.kptn.utils.communication.xauth.utils.SignatureEncode;
import fr.kptn.utils.communication.xauth.utils.enums.OAuthSignatureMethod;
import fr.kptn.utils.communication.xauth.utils.enums.OAuthVersion;

public class XAuthCommunicator {
	private String consumerKey;
	private String consumerSecret;
	private OAuthSignatureMethod signatureMethod;
	private OAuthVersion oauthVersion;
	private Map<String, String> parameterMap;
	private String method = "";
	private String apiUrl = "";
	private String oauthToken = null;
	private String oauthTokenSecret = null;
	private SortedMap<String, String> oauthParametersMap;

	protected XAuthCommunicator(String consumerSecret, String consumerKey,
			OAuthVersion oauthversion, OAuthSignatureMethod signatureMethod) {
		this.consumerSecret = consumerSecret;
		this.consumerKey = consumerKey;

		if (signatureMethod == null) {
			signatureMethod = OAuthSignatureMethod.SIGNATURE_METHOD_HMAC_SHA1;
		} else {
			this.signatureMethod = signatureMethod;
		}
		if (oauthversion == null) {
			this.oauthVersion = OAuthVersion.OAUTH_VERSION_1;
		} else {
			this.oauthVersion = oauthversion;
		}
	}

	public static XAuthCommunicator getAuthenticator(String consumerSecret,
			String consumerKey, OAuthVersion oauthversion,
			OAuthSignatureMethod signatureMethod)
			throws XAuthCommunicatorException {
		if (consumerKey == null || "".equals(consumerKey)) {
			throw new XAuthCommunicatorException(
					"Impossible de créer un authenticator sans clé");
		}
		if (consumerSecret == null || "".equals(consumerSecret)) {
			throw new XAuthCommunicatorException(
					"Impossible de créer un authenticator sans secret");
		}

		return new XAuthCommunicator(consumerSecret, consumerKey,
				oauthversion, signatureMethod);
	}

	public XAuthAccessInfos getAccessToken(String method, String apiUrl,
			XAuthParams xauthparams) throws XAuthCommunicatorException {
		XAuthAccessInfos accessTokenInfos = new XAuthAccessInfos();
		this.parameterMap = new TreeMap<String, String>();
		this.parameterMap
				.put("x_auth_username", xauthparams.getXauthUsername());
		this.parameterMap
				.put("x_auth_password", xauthparams.getXauthPassword());
		this.parameterMap.put("x_auth_mode", xauthparams.getXauthMode());
		this.apiUrl = apiUrl;
		this.method = method;

		try {
			HttpResponse lResponse = authenticate();
			if (lResponse == null) {
				throw new XAuthCommunicatorException(
						"Réponse vide pour la requête d'authentification");
			} else {
				int statusCode = lResponse.getStatusLine().getStatusCode();
				if (statusCode == HttpURLConnection.HTTP_OK) {
					String result = EntityUtils.toString(lResponse.getEntity(),
							"UTF-8");
					String[] parameters = result.split("&");
					for (String parameter : parameters) {
						String[] keyAndValue = parameter.split("=");
						if (keyAndValue.length < 2) {
							continue;
						}
						String key = keyAndValue[0];
						String value = keyAndValue[1];
						if ("oauth_token".equals(key)) {
							accessTokenInfos.setOauthToken(value);
							oauthToken = value;
						} else if ("oauth_token_secret".equals(key)) {
							accessTokenInfos.setOauthSecret(value);
							oauthTokenSecret = value;
						}
					}
				}
			}
		} catch (ParseException e) {
			throw new XAuthCommunicatorException(
					"Erreur de parsing des résultats de la requête d'authentification",
					e);
		} catch (IOException e) {
			throw new XAuthCommunicatorException(
					"Erreur d'entrée-sortie lors de la récupération des résultats de la requête d'authentification",
					e);
		}
		
		this.parameterMap.clear();

		return accessTokenInfos;
	}

	public HttpResponse authenticate() throws XAuthCommunicatorException {
		oauthParametersMap = createParametersMap();
		HttpResponse objResponse = null;

		HttpClient objHttp = new DefaultHttpClient();
		HttpPost objPost = new HttpPost(apiUrl);

		if (parameterMap != null && parameterMap.size() > 0) {
			HttpEntity entity = null;
			final List<NameValuePair> params = new ArrayList<NameValuePair>();

			for (Map.Entry<String, String> param : parameterMap.entrySet()) {
				params.add(new BasicNameValuePair(param.getKey(), param
						.getValue()));
			}

			try {
				entity = new UrlEncodedFormEntity(params, HTTP.UTF_8);
			} catch (UnsupportedEncodingException e) {
				throw new XAuthCommunicatorException(
						"Encoding UTF-8 non supporté pour l'encodage des paramètres de la requête d'authentification",
						e);
			}
			objPost.setEntity(entity);
		}

		try {
			objPost.addHeader("Authorization", createAuthorizationValue());
			objResponse = objHttp.execute(objPost);
		} catch (ClientProtocolException e) {
			throw new XAuthCommunicatorException(
					"Erreur de protocole lors de l'exécution de la requête d'authentification",
					e);
		} catch (IOException e) {
			throw new XAuthCommunicatorException(
					"Erreur d'entrée-sortie lors de l'exécution de la requête d'authentification",
					e);
		}

		return objResponse;
	}

	public HttpResponse requestPost(String apiUrl, Map<String, String> parameterMap) throws XAuthCommunicatorException {
		this.apiUrl = apiUrl;

		oauthParametersMap = createParametersMap();
		HttpResponse objResponse = null;

		HttpClient objHttp = new DefaultHttpClient();
		HttpPost objPost = new HttpPost(apiUrl);

		if (parameterMap != null && parameterMap.size() > 0) {
			HttpEntity entity = null;
			final List<NameValuePair> params = new ArrayList<NameValuePair>();

			for (Map.Entry<String, String> param : parameterMap.entrySet()) {
				params.add(new BasicNameValuePair(param.getKey(), param
						.getValue()));
			}

			try {
				entity = new UrlEncodedFormEntity(params, HTTP.UTF_8);
			} catch (UnsupportedEncodingException e) {
				throw new XAuthCommunicatorException(
						"Encoding UTF-8 non supporté pour l'encodage des paramètres de la requête POST",
						e);
			}
			objPost.setEntity(entity);
		}

		try {
			objPost.addHeader("Content-Type","application/x-www-form-urlencoded");
			objPost.addHeader("Authorization", createAuthorizationValue());
			objResponse = objHttp.execute(objPost);
		} catch (ClientProtocolException e) {
			throw new XAuthCommunicatorException(
					"Erreur de protocole lors de l'exécution de la requête POST",
					e);
		} catch (IOException e) {
			throw new XAuthCommunicatorException(
					"Erreur d'entrée lors de l'exécution de la requête POST", e);
		} finally {

		}
		return objResponse;
	}

	public HttpResponse requestPut() throws XAuthCommunicatorException {
		oauthParametersMap = createParametersMap();
		HttpResponse objResponse = null;

		HttpClient objHttp = new DefaultHttpClient();
		HttpPut objPut = new HttpPut(apiUrl);

		if (parameterMap != null && parameterMap.size() > 0) {
			HttpEntity entity = null;
			final List<NameValuePair> params = new ArrayList<NameValuePair>();

			for (Map.Entry<String, String> param : parameterMap.entrySet()) {
				params.add(new BasicNameValuePair(param.getKey(), param
						.getValue()));
			}

			try {
				entity = new UrlEncodedFormEntity(params, HTTP.UTF_8);
			} catch (UnsupportedEncodingException e) {
				throw new XAuthCommunicatorException(
						"Encoding UTF-8 non supporté pour l'encodage des paramètres de la requête PUT",
						e);
			}
			objPut.setEntity(entity);
		}

		try {
			objPut.addHeader("Authorization", createAuthorizationValue());
			objResponse = objHttp.execute(objPut);
		} catch (ClientProtocolException e) {
			throw new XAuthCommunicatorException(
					"Erreur de protocole lors de l'exécution de la requête PUT",
					e);
		} catch (IOException e) {
			throw new XAuthCommunicatorException(
					"Erreur d'entrée-sortie lors de l'exécution de la requête PUT",
					e);
		} finally {

		}
		return objResponse;
	}

	public HttpResponse requestDelete(String apiUrl) throws XAuthCommunicatorException {
		this.apiUrl = apiUrl;
		oauthParametersMap = createParametersMap();
		HttpResponse objResponse = null;

		HttpClient objHttp = new DefaultHttpClient();
		HttpDelete objDelete = new HttpDelete(apiUrl);

		try {
			objDelete.addHeader("Authorization", createAuthorizationValue());
			objResponse = objHttp.execute(objDelete);
		} catch (ClientProtocolException e) {
			throw new XAuthCommunicatorException(
					"Erreur de protocole lors de l'exécution de la requête DELETE",
					e);
		} catch (IOException e) {
			throw new XAuthCommunicatorException(
					"Erreur d'entrée-sortie lors de l'exécution de la requête DELETE",
					e);
		} finally {

		}
		return objResponse;
	}

	public HttpResponse requestGet(String apiUrl) throws XAuthCommunicatorException {
		this.apiUrl = apiUrl;

		oauthParametersMap = createParametersMap();
		HttpResponse objResponse = null;

		HttpClient objHttp = new DefaultHttpClient();
		HttpGet objGet = new HttpGet(apiUrl);
		try {
			objGet.addHeader("Authorization", createAuthorizationValue());
			objResponse = objHttp.execute(objGet);
		} catch (ClientProtocolException e) {
			throw new XAuthCommunicatorException(
					"Erreur de protocole lors de l'exécution de la requête GET",
					e);
		} catch (IOException e) {
			throw new XAuthCommunicatorException(
					"Erreur d'entrée-sortie lors de l'exécution de la requête GET",
					e);
		}

		return objResponse;
	}

	private SortedMap<String, String> createParametersMap() {
		SortedMap<String, String> map = new TreeMap<String, String>();
		if(oauthToken != null) {
			map.put("realm", "API");
		}
		map.put("oauth_consumer_key", consumerKey);
		map.put("oauth_nonce", UUID.randomUUID().toString());
		map.put("oauth_signature_method", signatureMethod.getMethodName());
		map.put("oauth_timestamp", getTimeStamp());
		map.put("oauth_version", oauthVersion.getVersion());
		if (oauthToken != null) {
			map.put("oauth_token", oauthToken);
		}
		return map;
	}

	private String getKey() {
		String result = "";
		StringBuilder builder = new StringBuilder();
		builder.append(consumerSecret);
		builder.append("&");
		if (oauthTokenSecret != null)
			builder.append(oauthTokenSecret);
		result = builder.toString();
		return result;
	}

	private String getTimeStamp() {
		return Long.toString(System.currentTimeMillis() / 1000);
	}

	private String createAuthorizationValue()
			throws XAuthCommunicatorException {
		/*
		 * http://oauth.net/core/1.0/#nonce 「5.4.1. Authorization Header�?
		 * Authorization Header�?�作�?
		 */
		String result = "";
		StringBuilder builder = new StringBuilder();
		builder.append("OAuth ");
		for (Map.Entry<String, String> param : oauthParametersMap.entrySet()) {
			builder.append(param.getKey() + "=");
			builder.append("\"" + param.getValue() + "\",");
		}
		builder.append("oauth_signature" + "=");
		builder.append("\"" + getSignature(getSignatureBaseString(), getKey())
				+ "\"");
		result = builder.toString();
		return result;
	}

	private String getSignatureBaseString() throws XAuthCommunicatorException {
		String result = null;
		try {
			result = method + "&" + encodeURL(apiUrl) + "&"
					+ SignatureEncode.encode(getRequestParameters());
		} catch (UnsupportedEncodingException e) {
			throw new XAuthCommunicatorException(
					"Erreur lors de la création de la base pour la signature",
					e);
		}

		return result;
	}

	private String encodeURL(String str) throws XAuthCommunicatorException {
		String result = null;
		try {
			result = URLEncoder.encode(str, "UTF-8");
		} catch (UnsupportedEncodingException e) {
			throw new XAuthCommunicatorException(
					"Erreur lors de l'encodage de l'URL (" + str + ")", e);
		}

		return result;
	}

	private String getRequestParameters() throws XAuthCommunicatorException {
		StringBuilder builder = new StringBuilder();
		try {
			if (parameterMap != null && parameterMap.size() > 0) {
				for (Map.Entry<String, String> param : parameterMap.entrySet()) {
					oauthParametersMap.put(
							SignatureEncode.encode(param.getKey()),
							SignatureEncode.encode(param.getValue()));
				}
			}
			for (Map.Entry<String, String> param : oauthParametersMap
					.entrySet()) {
				builder.append(param.getKey());
				builder.append("=");
				builder.append(param.getValue());
				builder.append("&");
			}
		} catch (UnsupportedEncodingException e) {
			throw new XAuthCommunicatorException(
					"Encoding non supporté lors de la génération des paramètres de la requête d'authentification",
					e);
		}

		return builder.toString().substring(0, builder.length() - 1);
	}

	private String getSignature(String signatureBaseString, String keyString)
			throws XAuthCommunicatorException {
		String result = null;
		try {
			String base = signatureBaseString;
			if(signatureMethod == OAuthSignatureMethod.PLAINTEXT) {
				result = encodeURL(consumerSecret + "&" + oauthTokenSecret);
			} else {
				Mac mac = Mac.getInstance(signatureMethod.getMethodAlgorithm());
				Key key = new SecretKeySpec(keyString.getBytes(),
						signatureMethod.getMethodAlgorithm());
				mac.init(key);
				byte[] digest = mac.doFinal(base.getBytes());
				result = encodeURL(Base64.encodeBytes(digest));
			}
		} catch (NoSuchAlgorithmException e) {
			throw new XAuthCommunicatorException("Algorithme spécifié ("
					+ signatureMethod.getMethodAlgorithm()
					+ ") indisponible pour génération de la signature", e);
		} catch (InvalidKeyException e) {
			throw new XAuthCommunicatorException(
					"Clé invalide pour la génération de la signature", e);
		}

		return result;
	}

	public OAuthSignatureMethod getSignatureMethod() {
		return signatureMethod;
	}

	public void setSignatureMethod(OAuthSignatureMethod signatureMethod) {
		this.signatureMethod = signatureMethod;
	}

}
